﻿namespace MvvmTools
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using System.Linq.Expressions;
    using JetBrains.Annotations;

    /// <summary>
    /// The expression extensions.
    /// </summary>
    public static class ExpressionExtensions
    {
        private static readonly Dictionary<ExpressionKey, string> Names = new Dictionary<ExpressionKey, string>();

        /// <summary>
        /// Returns name of parameter in expression.
        /// </summary>
        /// <param name="expression">
        /// The expression to extract parameter name.
        /// </param>
        /// <returns>
        /// Parameter name.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// Specified expression is null.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// Parameter name is null or zero length.
        /// </exception>
        /// <example>
        /// Retrieving parameter name from expression.
        /// <code>
        /// class Test
        /// {
        ///     public bool SomeFlag { get; set; }
        /// 
        ///     void ExtractName(Func&lt;Expression&lt;Func&lt;T&gt;&gt;&gt; reference)
        ///     {
        ///         Console.WriteLine(reference().GetParameterName());
        ///     }
        /// 
        ///     void Run()
        ///     {
        ///         ExtractName(() => () => SomeFlag);  // Write to console "SomeFlag" string
        ///     }
        /// }
        /// </code>
        /// </example>
        [NotNull] public static string GetParameterName([NotNull] this Expression expression)
        {
            Contract.Requires<ArgumentNullException>(expression != null);
            Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>()));
            
            var finder = new ParameterNamesFinder();
            var parameterNames = finder.GetParameterNames(expression);
            if (parameterNames.Length == 0)
            {
                throw new InvalidOperationException("Unable to retrieve name");
            }
            
            return parameterNames[0];
        }

        /// <summary>
        /// Returns name of parameter in expression.
        /// </summary>
        /// <typeparam name="TTarget">
        /// The type of the target.
        /// </typeparam>
        /// <typeparam name="TProperty">
        /// The type of the property.
        /// </typeparam>
        /// <param name="expression">
        /// The expression to extract parameter name.
        /// </param>
        /// <returns>
        /// Parameter name.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// Specified expression is null.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// Parameter name is null or zero length.
        /// </exception>
        /// <example>
        /// Retrieving parameter name from expression.
        /// <code>
        /// class Test
        /// {
        ///     public bool SomeFlag { get; set; }
        /// 
        ///     void ExtractName(Expression&lt;Func&lt;TTarget, TProperty&gt;&gt; reference)
        ///     {
        ///         Console.WriteLine(reference().GetParameterName());
        ///     }
        /// 
        ///     void Run()
        ///     {
        ///         ExtractName(this => this.SomeFlag);  // Write to console "SomeFlag" string
        ///     }
        /// }
        /// </code>
        /// </example>
        [NotNull] public static string GetParameterName<TTarget, TProperty>([NotNull] this Expression<Func<TTarget, TProperty>> expression)
        {
            Contract.Requires<ArgumentNullException>(expression != null);
            Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>()));

            var key = new ExpressionKey(typeof(TTarget), expression.Body.ToString());
            string name;
            lock (Names)
            {
                if (!Names.TryGetValue(key, out name))
                {
                    Names[key] = name = GetParameterName((Expression)expression);
                }
            }

            return name;
        }

        /// <summary>
        /// Returns name of parameter in expression.
        /// </summary>
        /// <param name="expression">
        /// The expression to extract parameter name.
        /// </param>
        /// <typeparam name="TProperty">
        /// The type of the property.
        /// </typeparam>
        /// <param name="targetType">
        /// The type of the target.
        /// </param>
        /// <returns>
        /// Parameter name.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// Specified expression or specified target type are null.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// Parameter name is null or zero length.
        /// </exception>
        /// <example>
        /// Retrieving parameter name from expression.
        /// <code>
        /// class Test
        /// {
        ///     public bool SomeFlag { get; set; }
        /// 
        ///     void ExtractName(Expression&lt;Func&lt;TProperty&gt;&gt; reference)
        ///     {
        ///         Console.WriteLine(reference().GetParameterName(GetType()));
        ///     }
        /// 
        ///     void Run()
        ///     {
        ///         ExtractName(() => SomeFlag);  // Write to console "SomeFlag" string
        ///     }
        /// }
        /// </code>
        /// </example>
        [NotNull] public static string GetParameterName<TProperty>([NotNull] this Expression<Func<TProperty>> expression, [NotNull] Type targetType)
        {
            Contract.Requires<ArgumentNullException>(expression != null);
            Contract.Requires<ArgumentNullException>(targetType != null);
            Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>()));

            var key = new ExpressionKey(targetType, expression.Body.ToString());
            string name;
            lock (Names)
            {
                if (!Names.TryGetValue(key, out name))
                {
                    Names[key] = name = GetParameterName(expression);
                }
            }

            return name;
        }

        private struct ExpressionKey : IEquatable<ExpressionKey>
        {
            private static readonly IEqualityComparerEx<ExpressionKey> EqualityComparer = EqualityComparer<ExpressionKey>
                .Depends(i => i._targetType)
                .Depends(i => i._body)
                .Create();

            private readonly Type _targetType;
            private readonly string _body;

            public ExpressionKey([NotNull] Type targetType, [NotNull] string body)
            {
                Contract.Requires<ArgumentNullException>(targetType != null);
                Contract.Requires<ArgumentNullException>(body != null);

                _targetType = targetType;
                _body = body;
            }

            public static bool operator ==(ExpressionKey left, ExpressionKey right)
            {
                return left.Equals(right);
            }

            public static bool operator !=(ExpressionKey left, ExpressionKey right)
            {
                return !left.Equals(right);
            }

            public bool Equals(ExpressionKey other)
            {
                return EqualityComparer.Equals(this, other);
            }

            public override bool Equals(object obj)
            {
                return EqualityComparer.Equals(this, obj);
            }

            public override int GetHashCode()
            {
                return EqualityComparer.GetHashCode(this);
            }
        }

        private class ParameterNamesFinder : ExpressionVisitor
        {
            private readonly IList<string> _parameterNames = new List<string>();

            public string[] GetParameterNames([NotNull] Expression expression)
            {
                Contract.Requires<ArgumentNullException>(expression != null);
                Contract.Ensures(Contract.Result<string[]>() != null);

                Visit(expression);
                return _parameterNames.ToArray();
            }

            protected override Expression VisitMember(MemberExpression node)
            {
                Contract.Ensures(Contract.Result<Expression>() != null);

                _parameterNames.Add(node.Member.Name);
                return base.VisitMember(node);
            }
        }
    }
}